Skip to content

Harden the release and site workflows, add a security policy, and update dependencies with security fixes - #26

Merged
ndbroadbent merged 12 commits into
mainfrom
nathan/repo-hardening
Oct 18, 2026
Merged

ndbroadbent merged 12 commits into
mainfrom
nathan/repo-hardening

Conversation

@ndbroadbent

@ndbroadbent ndbroadbent commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Eleventh of twelve stacked PRs for 0.2.0.

Releases

  • A release runs only for a pushed v* tag, which only admins can push. The release: published trigger is gone, so
    a release no longer publishes twice.
  • Publishing has its own jobs in the release environment, which needs an approval. They get the built gem and the
    provider changes as artifacts, so no job that holds a credential runs the repository's code or restores a cache.
  • The gem is pushed with a short-lived key from RubyGems trusted publishing (OIDC), instead of the long-lived
    RUBYGEMS_API_KEY secret.
  • The provider repository is public, so it's cloned without a token. The token is passed to the two pushes only.

Workflows and the site

  • Every action is pinned to a commit (a tag can be moved), checkouts don't keep the token in .git/config, and the
    CI, Rails main and Terraform example workflows get read-only tokens.
  • The site deploy served another branch's coverage report. It downloaded the coverage artifact from the newest
    successful CI run of any branch, forks included (dawidd6/action-download-artifact v3, whose allow_forks
    defaults to true, GHSA-5xr6-xhww-33m4), and served it under logstruct.com/coverage/. CI now calls the deploy as
    its last job on a push to main, with that run's artifact and commit. Only the deploy job can write to Pages.
  • The site's build broke when the gemspec's dependencies changed (it throws for a dependency it can't describe), and
    CI didn't build it. CI builds the site the way the deploy does, and a test checks every gemspec dependency has a
    description.
  • The Playwright static server listened on every interface and served any file a ../ path reached. It binds
    127.0.0.1 and serves only files inside out/.
  • The gem packages only files committed to git (Dir[] picked up untracked files under lib/ in a local build).

Security policy and dependencies

  • SECURITY.md asks for private reports through GitHub's vulnerability reporting.
  • Dependabot checks the gems, both pnpm projects and the workflow actions weekly, waits 7 days before proposing a new
    release (security updates aren't delayed), and groups minor and patch updates.
  • Rails 8.0.5.1 and the gems with open Dependabot alerts (rack, nokogiri, loofah, rails-html-sanitizer, json,
    net-imap, mail and others), with their RBIs. json stays below 3: ActiveSupport 8.0 passes quirks_mode, which
    json 3 rejects.
  • The docs site: Next 15.5.27, react-syntax-highlighter 16 (prismjs 1.30), sharp 0.35, and postcss 8.5 for Next (it
    pins 8.4.31, which has open advisories). Removed nine packages nothing imports.
  • cspell and prettier updated. jscpd stays on 4.0.7: 4.3 needs Node 22.12 or later.

Before the first release from this workflow

Repository and RubyGems settings this needs (none of them are in the code):

  • A trusted publisher on rubygems.org for logstruct: this repository, release.yml, environment release.
  • The release environment with required reviewers, and TF_PROVIDER_GITHUB_TOKEN as its secret.
  • The "Release tags: admins only" ruleset for v* tags.
  • Afterwards, revoke the old RubyGems API key and delete the RUBYGEMS_API_KEY secret.

Testing

Sorbet and the unit suite (367 tests) pass at this branch's tip. actionlint passes on every
workflow. The deploy and release jobs run only on main and on tags, so they're first exercised by the merge and the
0.2.0 release (the release workflow's dry_run input runs everything but the publishing jobs).


Stack for 0.2.0, merged in order: #16 → #17 → #18 → #19 → #20 → #21 → #22 → #23 → #24 → #25 → #26 → #27

Every action is pinned to the commit of its latest release in the major version
we used (a tag can be moved to other code), checkouts don't keep the token in
.git/config, and the CI, Rails main and Terraform example workflows get a
read-only token instead of the repository default.
- A release runs only for a pushed v* tag (only admins can push them). The
  release: published trigger is gone, so a release no longer publishes twice.
- Publishing moves to its own jobs in the release environment, which needs an
  approval and gets the built gem and the provider changes as artifacts: no
  job that has a credential runs the repository's code or restores a cache.
- The gem is pushed with a short-lived key from RubyGems trusted publishing
  (OIDC) instead of the long-lived RUBYGEMS_API_KEY secret.
- The provider repository is public, so it's cloned without a token. The
  token is passed to the two pushes only, not stored in the clone's config.
The supported versions page throws for a gemspec dependency it has no
description for, so the site stopped building when the gemspec listed
actionpack, activesupport, globalid and railties instead of rails. CI's docs
job only type-checks and runs the tests, so nothing caught it.

The descriptions move to docs/lib/gem-descriptions.ts with a test that every
gemspec dependency has one, and a new CI job builds the site the way the
deploy does, with the run's coverage report and YARD docs.
The deploy downloaded coverage-ruby-rails-latest from the newest successful CI
run of any branch, forks included (dawidd6/action-download-artifact v3, whose
allow_forks defaults to true, GHSA-5xr6-xhww-33m4), and served it under
logstruct.com/coverage/. A fork's pull request could upload its own HTML under
that name.

CI now calls the deploy as its last job on a push to main, and the deploy
downloads that run's artifact and checks out that run's commit. A manual
deployment uses the latest successful CI run of a push to main, which a pull
request's run never is. Only the deploy job can write to Pages.
The Playwright static server listened on every interface and served any file
the request path reached with ../ (encoded or not) or through a symlink. It now
binds 127.0.0.1, resolves each path inside out/ and answers 404 otherwise.

The home page test expected the old heading.
SECURITY.md asks for private reports through GitHub's vulnerability reporting.
Dependabot checks the gems, both pnpm projects and the workflow actions weekly,
with minor and patch updates grouped into one pull request each.
Relaxes the rails pin to 8.0.5 and updates only the gems with open Dependabot
alerts (rack, nokogiri, the Rails gems, loofah, rails-html-sanitizer, json,
net-imap, mail and others), regenerating their RBIs.

The concurrent-ruby <= 1.3.4 pin was only for Rails 7.0, whose test app pins
it itself, so it goes. json stays below 3: ActiveSupport 8.0 passes
quirks_mode, which json 3 rejects. net-imap stays on 0.5.x, and two of its
classes' new parents are allowed in sorbet/config.
Next 15.5.27, react-syntax-highlighter 16 (prismjs 1.30) and sharp 0.35, plus
pnpm update within each range. Removes packages nothing imports: gray-matter,
next-mdx-remote, remark, remark-html, rehype-highlight, rehype-pretty-code,
shiki, copy-to-clipboard and framer-motion, and the optional Linux binaries
pinned to latest for npm (pnpm installs the matching ones itself; checked
with a linux/amd64 install and build).

Biome 2.5 sorts export lists and flags an index key and a missed optional
chain. Jest 30.5 exits 0 without running tests when watchman is broken, so
the tests don't use it.
jscpd stays on 4.0.7: 4.3 needs Node 22.12 or later.
Dir[] picked up any untracked file under lib/ in a local build. The gem now
takes the same patterns from git ls-files, and uses Dir only without git.
Gives a compromised release time to be noticed and pulled first. Security
updates aren't delayed.
Next 15.5 pins postcss 8.4.31, which has open advisories (they need
attacker-controlled CSS or source maps; the site builds only its own CSS).
Next 16 moved to 8.5.
@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: a5bcfba3-2155-4645-b9fc-38a80427c9b6
📥 Commits

Reviewing files that changed from the base of the PR and between 9f15d27 and 4c48410.

⛔ Files ignored due to path filters: 31
  • Gemfile.lock is excluded by !**/*.lock
  • docs/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
  • sorbet/config is excluded by !sorbet/**
  • sorbet/rbi/gems/actioncable@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/actionmailbox@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/actionmailer@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/actionpack@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/actiontext@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/actionview@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/activejob@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/activemodel@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/activerecord@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/activestorage@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/activesupport@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/addressable@2.9.0.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/carrierwave@3.1.4.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/concurrent-ruby@1.3.8.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/json@2.21.2.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/loofah@2.25.2.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/mail@2.9.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/net-imap@0.5.15.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/nokogiri@1.19.4.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/rack-session@2.1.2.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/rack@3.2.7.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/rails-html-sanitizer@1.7.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/rails@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/railties@8.0.5.1.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/tsort@0.2.0.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/uri@1.1.2.rbi is excluded by !sorbet/**
  • sorbet/rbi/gems/websocket-driver@0.8.2.rbi is excluded by !sorbet/**
📒 Files selected for processing: 29
  • .github/dependabot.yml
  • .github/workflows/deploy-docs.yml
  • .github/workflows/rails-main.yml
  • .github/workflows/release.yml
  • .github/workflows/terraform-example.yml
  • .github/workflows/test.yml
  • Gemfile
  • SECURITY.md
  • docs/app/docs/comparison/page.tsx
  • docs/app/docs/supported-versions/page.tsx
  • docs/biome.json
  • docs/components/ui/accordion.tsx
  • docs/components/ui/card.tsx
  • docs/components/ui/dropdown-menu.tsx
  • docs/components/ui/navigation-menu.tsx
  • docs/components/ui/recharts.tsx
  • docs/components/ui/sheet.tsx
  • docs/components/ui/tabs.tsx
  • docs/e2e/playwright/home.spec.ts
  • docs/jest.config.ts
  • docs/lib/__tests__/gem-descriptions.test.ts
  • docs/lib/gem-descriptions.ts
  • docs/package.json
  • docs/playwright.config.ts
  • docs/pnpm-workspace.yaml
  • docs/scripts/__tests__/serve-static.test.js
  • docs/scripts/serve-static.js
  • logstruct.gemspec
  • package.json
+---------------------------------------------------------------------+
| This is 'temporary' in the same way legacy systems are 'temporary'. |
+---------------------------------------------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant